還記得嗎?你是負責 Imager 的前端工程師,上次做了 Lazy Loading 改善了資源浪費的問題,公司對你的表現非常的滿意。
但有幹勁的你決定想想看,有沒有其他方案也可以達到節約資源的效果,可以的話甚至可以和現在的 Lazy Loading 做結合!
一邊品嚐著香醇濃郁的咖啡,一邊滑著公司配給你的 MacBook Pro,一番尋尋覓覓之後,你找到了一個很酷的技術方案——
你稍微把文章掃過一遍,歸納出幾個實作重點:
身為一名工程師,最快樂的時光就是動手寫程式的時候啦。如果你喜歡直接看 Code 來理解的話可以直接前往完成的範例觀看:
考量到 Lazy Loading 與 Infinite List 的結合較為複雜,這次會先實作只有 Infinite List 的版本,下一篇再將兩者結合!
<InfiniteListTrigger />
首先我們需要一個 Element 搭配 Intersection Observer 來觸發 Loading 的行為,也就是當這個 Element 出現在畫面上時,就觸發下載新的圖片資料。
我們就將這個 Element 命名為 <InfiniteListTrigger />
,接著確保 Loading 狀態時不會顯示它,目的是避免重複觸發下載圖片列表。
// App.js
import React, { useRef } from "react";
...
import {
...
Loading,
InfiniteListTrigger
} from "./style";
const App = () => {
const triggerRef = useRef(null);
...
return (
<View>
...
{/* 並且為了防止重複觸發圖片下載,在 Loading 時我們就不顯示觸發用的 Element */}
{!isLoading && <InfiniteListTrigger ref={triggerRef} />}
</View>
);
};
export default App;
fetchNewImages()
接著我們來寫一個模擬打 API 的 function,他做的事情很單純,就是等一秒之後拿到新的圖片列表。
// App.js
...
const App = () => {
...
const fetchNewImages = () =>
// 回傳一個 Promise 是為了模擬打 API 時非同步的情境
new Promise((resolve) => {
const newImages = generateImages({ count: 10 });
// 我們假定每次打 API 平均一秒後拿到新的圖片列表
setTimeout(() => {
setImages((prev) => [...prev, ...newImages]);
resolve();
}, 1000);
});
...
};
...
useInifiteList()
現在有了元件之後,我們需要一個 hook 將 <InifiniteListTrigger />
交給 Intersection Observer 監聽。
這次要做的是相對簡單,我們需要 ref 和 onView 來完成無限加載的行為,我們看看它們對應的工作:
<InfiniteListTrigger />
。// useInfiniteList.js
import { useState, useEffect } from "react";
const useInfiniteList = ({ ref, onView }) => {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// 要監聽的元件
const trigger = ref.current;
if (!trigger) return;
if (!onView) return;
// 這次我們的監聽行為非常的簡單
// 如果元件出現在畫面上就觸發 onView 事件
const callback = (entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
setIsLoading(true);
await onView();
setIsLoading(false);
}
});
};
// 開始監聽元件
const observer = new IntersectionObserver(callback);
observer.observe(trigger);
// 別忘了取消監聽元件哦!
return () => {
observer.unobserve(trigger);
};
}, [ref, isLoading, onView]);
return { isLoading };
};
export default useInfiniteList;
現在我們將上面的功能都組合在一起!
// App.js
import React, { useRef, useState } from "react";
import Image from "./components/Image";
import useCount from "./hooks/useCount";
import useInfiniteList from "./hooks/useInfiniteList";
import generateImages from "./utils/generateImages";
import {
View,
Title,
Loading,
ImageBlock,
ImageCount,
InfiniteListTrigger
} from "./style";
const App = () => {
const triggerRef = useRef(null);
const [images, setImages] = useState([]);
const { count, addCount } = useCount();
// 這裡我們製作了一個下載圖片列表的 function
// 當使用者看到 <InfiniteListTrigger /> 的時候就觸發下載圖片列表
const fetchNewImages = () =>
// 回傳一個 Promise 是為了模擬打 API 時非同步的情境
new Promise((resolve) => {
const newImages = generateImages({ count: 10 });
// 我們假定每次打 API 平均一秒後拿到新的圖片列表
setTimeout(() => {
setImages((prev) => [...prev, ...newImages]);
resolve();
}, 1000);
});
// ref : 監聽的元件
// onView: 當元件出現在畫面上時,執行此事件
const { isLoading } = useInfiniteList({
ref: triggerRef,
onView: fetchNewImages
});
return (
<View>
<ImageCount>圖片載入數量:{count}</ImageCount>
<Title>Imager</Title>
<ImageBlock>
{images.map((image) => (
<Image key={image.id} src={image.src} onLoad={addCount} />
))}
</ImageBlock>
{/* 當我們在下載新的圖片列表時,顯示 Loading 讓使用者知道圖片正在下載 */}
{isLoading && <Loading>Loading...</Loading>}
{/* 並且為了防止重複觸發圖片下載,在 Loading 時我們就不顯示觸發用的 Element */}
{!isLoading && <InfiniteListTrigger ref={triggerRef} />}
</View>
);
};
export default App;
你埋首在程式之中也經過了好一段時間,現在可以來看看最終成果了!
看起來運作的很順利,當頁面滑到最下方之後,我們才開始下載下面 10 筆的資料。看到這樣的成果,你不禁露出了驕傲的神情。
快樂的時光總是過得特別快,從肚子傳出的咕嚕聲將你的注意力拉回現實,居然已經是下班時間了!你緩緩的從座位站起身子,拿著錢包準備去吃晚餐啦!